/**
 * @license
 * Copyright 2019 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
'use strict';

/**
 * @fileoverview A collection of eslint rules written specifically for
 * Lighthouse. These are included by the eslint-plugin-local-rules plugin.
 */

const path = require('path');

/** @typedef {import('eslint').Rule.RuleModule} RuleModule */

/**
 * Use `require.resolve()` to resolve the location of `path` from a location of
 * `baseDir` and return it. Returns null if unable to resolve a path.
 * @param {string} path
 * @param {string} baseDir
 * @return {string|null}
 */
function requireResolveOrNull(path, baseDir) {
  try {
    return require.resolve(path, {
      paths: [baseDir],
    });
  } catch (err) {
    return null;
  }
}

/**
 * An eslint rule ensuring that any require() of a local path (aka not a core
 * module or a module dependency) includes a file extension (.js' or '.json').
 * @type {RuleModule}
 */
const requireFileExtension = {
  meta: {
    docs: {
      description: 'disallow require() without a file extension',
      category: 'Best Practices',
      recommended: false,
    },
    schema: [],
    fixable: 'code',
  },

  create(context) {
    return {
      CallExpression(node) {
        // Only look at instances of `require(moduleName: string)`.
        if (node.type !== 'CallExpression') return;
        if (node.callee.type !== 'Identifier' || node.callee.name !== 'require') return;
        if (!node.arguments.length) return;
        const arg0 = node.arguments[0];
        if (arg0.type !== 'Literal' || typeof arg0.value !== 'string') return;

        const requiredPath = arg0.value;

        // If it's not a local file, we don't care.
        if (!requiredPath.startsWith('.')) return;

        // Check that `requiredPath` is resolvable from the source file.
        const contextDirname = path.dirname(context.getFilename());
        const resolvedRequiredPath = requireResolveOrNull(requiredPath, contextDirname);
        if (!resolvedRequiredPath) {
          return context.report({
            node: node,
            message: `Cannot resolve module '${requiredPath}'.`,
          });
        }

        // If it has a file extension, it's good to go.
        if (requiredPath.endsWith('.js')) return;
        if (requiredPath.endsWith('.json')) return;

        context.report({
          node: node,
          message: 'Local require path must have a file extension.',
          fix(fixer) {
            // Find the correct file extension/filename ending of the requiredPath.
            let fixedPath = path.relative(contextDirname, resolvedRequiredPath);
            if (!fixedPath.startsWith('.')) fixedPath = `./${fixedPath}`;

            // Usually `fixedPath.startsWith(requiredPath)` and this will just add
            // a suffix to the existing path, but sometimes humans write confusing
            // paths, e.g. './core/lib/../lib/lh-error.js'. To cover both
            // cases, double check that the paths resolve to the same file.
            const resolvedFixedPath = requireResolveOrNull(fixedPath, contextDirname);

            // If somehow they don't point to the same file, don't try to fix.
            if (resolvedFixedPath !== resolvedRequiredPath) return null;

            return fixer.replaceText(arg0, `'${fixedPath}'`);
          },
        });
      },
    };
  },
};

module.exports = {
  'require-file-extension': requireFileExtension,
};
